<html>
<!-- https://github.com/samirkumardas/pcm-player -->

<head>
	<script>
		function floatTo16BitPCM(output, offset, input) {
			for (let i = 0; i < input.length; i++, offset += 2) {
				let s = Math.max(-1, Math.min(1, input[i]));
				output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
			}
		}

		const samples = 512;
		const buffer = new ArrayBuffer(samples * 2); // for 16bitPCM
		const view = new DataView(buffer); // converto DataView

		const port = 9000
		let ws = null;
		let g_stream = null;
		let g_source = null;
		let ac = null;
		let player = null;
		let source = null;
		let sending = false;

		function startMic() {
			// use a separate player just to keep simple
			player = new PCMPlayer({
				encoding: '16bitInt', // or 32bitFloat
				channels: 1,
				sampleRate: 16000,
				flushingTime: 2000
			});

			navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
				ac = new AudioContext({
					sampleRate: 16000
				});
				g_stream = stream;
				source = ac.createMediaStreamSource(stream);
				// ok, this is desprecated but the new AudioWorkletNode is too heavy to use
				let scriptNode = ac.createScriptProcessor(samples, 1, 1);
				source.connect(scriptNode);
				let dest = ac.createMediaStreamDestination();
				scriptNode.connect(dest);
				// or directly connect to ac.destination to listen output buffer audio
				// scriptNode.connect(ac.destination);

				scriptNode.onaudioprocess = function (audioProcessingEvent) {
					console.log(audioProcessingEvent);
					var inputBuffer = audioProcessingEvent.inputBuffer;
					var outputBuffer = audioProcessingEvent.outputBuffer;
					// 遍历通道处理数据，当前只有1个输入1个输出
					for (var channel = 0; channel < outputBuffer.numberOfChannels; channel++) {
						if (channel > 0) break;
						var inputData = inputBuffer.getChannelData(channel);
						var outputData = outputBuffer.getChannelData(channel);
						console.log(inputData[0]);
						// player.feed(inputData);
						floatTo16BitPCM(view, 0, inputData);
						if (sending) send_audio(view);
						// player.feed(view); // test play directly
						// it is also possible to directly fill the output buffer when connect(ac.destination);
						// for (let i = 0; i < inputData.length; i++) {
						// 	outputData[i] = inputData[i];
						// }
					};
				}
			});
		}

		function stop() {
			sending = false;
			if (source) source.disconnect();
			source = null;
			if (ac) ac.close();
			if (g_stream) g_stream.getTracks().forEach(function (track) {
				console.log(track);
				track.stop();
			});
			g_stream = null;
			ac = null;
			if (player) player.destroy();
			player = null;
			if (ws) ws.close();
			ws = null;
			printLog("已停止音频传输和WebSocket连接");
		}

		function start() {
			ws = new WebSocket('ws://localhost:' + port);
			ws.binaryType = "arraybuffer";

			id = 0;

			const audio_params = {
				rate: 16000,
				codec: "L16",
				ms: 100,
			}

			ws.onclose = function (e) {
				const logMsg = "WebSocket连接已关闭: " + JSON.stringify(e);
				printLog(logMsg);
				sending = false;
				stop();
			};

			ws.onerror = function (e) {
				const logMsg = "WebSocket错误: " + JSON.stringify(e);
				printLog(logMsg);
				stop();
			};

			ws.onopen = function open() {
				const logMsg = "WebSocket连接已建立";
				printLog(logMsg);

				rpc = {
					jsonrpc: "2.0",
					id: id++,
					method: "start",
					params: audio_params
				}

				data = JSON.stringify(rpc);
				const sendMsg = "发送消息: " + data;
				printLog(sendMsg);
				ws.send(data);
			}

			ws.onmessage = function message(e) {
				const isBinary = e.data instanceof ArrayBuffer
				const recvMsg = '收到消息: isBinary=' + isBinary + ' type=' + typeof e.data + ' length=' + (e.data.length || e.data.byteLength);
				printLog(recvMsg);

				if (isBinary) {
					player.feed(new Int16Array(e.data));
					printLog("收到音频数据，长度: " + e.data.byteLength + " 字节");
				} else {
					rpc = JSON.parse(e.data)
					printLog("收到JSON消息: " + JSON.stringify(rpc, null, 2));
					if (rpc.result?.code == 200) {
						printLog("会话启动成功，开始音频传输");
						sending = true;
						startMic();
					}
				}
			}
		}

		function send_audio(array) {
			ws.send(array); // send as binary
			printLog("发送音频数据，长度: " + array.byteLength + " 字节");
		}

		// 日志记录函数
		function printLog(message) {
			console.log(message);
			const logArea = document.getElementById('logArea');
			if (logArea) {
				const timestamp = new Date().toLocaleTimeString();
				const logEntry = `[${timestamp}] ${message}\n`;
				logArea.value += logEntry;
				// 自动滚动到底部
				logArea.scrollTop = logArea.scrollHeight;
			}
		}

		// 清空日志
		function clearLog() {
			const logArea = document.getElementById('logArea');
			if (logArea) {
				logArea.value = '';
			}
		}
	</script>
</head>

<body>
	<div style="margin: 20px;">
		<h2>WebSocket音频通信测试</h2>

		<div style="margin-bottom: 20px;">
			<button onclick="start()"
				style="padding: 10px 20px; margin-right: 10px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Start</button>
			<button onclick="stop()"
				style="padding: 10px 20px; margin-right: 10px; background-color: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Stop</button>
			<button onclick="clearLog()"
				style="padding: 10px 20px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer;">清空日志</button>
		</div>

		<div style="margin-bottom: 20px;">
			<label for="logArea" style="display: block; margin-bottom: 10px; font-weight: bold;">通信日志:</label>
			<textarea id="logArea" readonly
				style="width: 100%; height: 300px; padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-family: monospace; font-size: 12px; background-color: #f9f9f9; resize: vertical;"></textarea>
		</div>

		<div style="font-size: 12px; color: #666;">
			<p><strong>使用说明:</strong></p>
			<ol>
				<li>点击 "Start" 按钮建立WebSocket连接并开始音频传输</li>
				<li>点击 "Stop" 按钮停止音频传输并关闭连接</li>
				<li>点击 "清空日志" 按钮清除日志显示区域</li>
				<li>日志区域会实时显示所有WebSocket通信过程和错误信息</li>
			</ol>
		</div>
	</div>

	<script type="text/javascript" src="pcm-player.min.js"></script>
</body>

</html>